/*
 * NPlot - A charting library for .NET
 * 
 * StepPlot.cs
 * Copyright (C) 2003-2006 Matt Howlett and others.
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Drawing;

namespace NPlot
{
    /// <summary>
    /// Encapsulates functionality for plotting data as a stepped line.
    /// </summary>
    public class StepPlot : BaseSequencePlot, IPlot, ISequencePlot
    {
        private bool center_;
        private bool hideHorizontalSegments_;
        private bool hideVerticalSegments_;
        private Pen pen_ = new Pen(Color.Black);
        private float scale_ = 1.0f;

        /// <summary>
        /// Constructor.
        /// </summary>
        public StepPlot()
        {
            Center = false;
        }

        /// <summary>
        /// Gets or sets whether or not steps should be centered. If true, steps will be centered on the
        /// X abscissa values. If false, the step corresponding to a given x-value will be drawn between
        /// this x-value and the next x-value at the current y-height.
        /// </summary>
        public bool Center
        {
            set { center_ = value; }
            get { return center_; }
        }

        /// <summary>
        /// The pen used to draw the plot
        /// </summary>
        public Pen Pen
        {
            get { return pen_; }
            set { pen_ = value; }
        }

        /// <summary>
        /// The color of the pen used to draw lines in this plot.
        /// </summary>
        public Color Color
        {
            set
            {
                if (pen_ != null)
                {
                    pen_.Color = value;
                }
                else
                {
                    pen_ = new Pen(value);
                }
            }
            get { return pen_.Color; }
        }

        /// <summary>
        /// If true, then vertical lines are hidden.
        /// </summary>
        public bool HideVerticalSegments
        {
            get { return hideVerticalSegments_; }
            set { hideVerticalSegments_ = value; }
        }

        /// <summary>
        /// If true, then vertical lines are hidden.
        /// </summary>
        public bool HideHorizontalSegments
        {
            get { return hideHorizontalSegments_; }
            set { hideHorizontalSegments_ = value; }
        }

        /// <summary>
        /// The horizontal line length is multiplied by this amount. Default
        /// corresponds to a value of 1.0.
        /// </summary>
        public float WidthScale
        {
            get { return scale_; }
            set { scale_ = value; }
        }

        /// <summary>
        /// Draws the step plot on a GDI+ surface against the provided x and y axes.
        /// </summary>
        /// <param name="g">The GDI+ surface on which to draw.</param>
        /// <param name="xAxis">The X-Axis to draw against.</param>
        /// <param name="yAxis">The Y-Axis to draw against.</param>
        public virtual void Draw(Graphics g, PhysicalAxis xAxis, PhysicalAxis yAxis)
        {
            SequenceAdapter data =
                new SequenceAdapter(DataSource, DataMember, OrdinateData, AbscissaData);

            double leftCutoff = xAxis.PhysicalToWorld(xAxis.PhysicalMin, false);
            double rightCutoff = xAxis.PhysicalToWorld(xAxis.PhysicalMax, false);

            for (int i = 0; i < data.Count; ++i)
            {
                PointD p1 = data[i];
                if (Double.IsNaN(p1.X) || Double.IsNaN(p1.Y))
                {
                    continue;
                }

                PointD p2;
                PointD p3;
                if (i + 1 != data.Count)
                {
                    p2 = data[i + 1];
                    if (Double.IsNaN(p2.X) || Double.IsNaN(p2.Y))
                    {
                        continue;
                    }
                    p2.Y = p1.Y;
                    p3 = data[i + 1];
                }
                else
                {
                    // Check that we are not dealing with a DataSource of 1 point.
                    // This check is done here so it is only checked on the end
                    // condition and not for every point in the DataSource.
                    if (data.Count > 1)
                    {
                        p2 = data[i - 1];
                    }
                    else
                    {
                        // TODO: Once log4net is set up post a message to the user that a step-plot of 1 really does not make any sense.
                        p2 = p1;
                    }

                    double offset = p1.X - p2.X;
                    p2.X = p1.X + offset;
                    p2.Y = p1.Y;
                    p3 = p2;
                }

                if (center_)
                {
                    double offset = (p2.X - p1.X)/2.0f;
                    p1.X -= offset;
                    p2.X -= offset;
                    p3.X -= offset;
                }

                PointF xPos1 = xAxis.WorldToPhysical(p1.X, false);
                PointF yPos1 = yAxis.WorldToPhysical(p1.Y, false);
                PointF xPos2 = xAxis.WorldToPhysical(p2.X, false);
                PointF yPos2 = yAxis.WorldToPhysical(p2.Y, false);
                PointF xPos3 = xAxis.WorldToPhysical(p3.X, false);
                PointF yPos3 = yAxis.WorldToPhysical(p3.Y, false);

                // do horizontal clipping here, to speed up
                if ((p1.X < leftCutoff || p1.X > rightCutoff) &&
                    (p2.X < leftCutoff || p2.X > rightCutoff) &&
                    (p3.X < leftCutoff || p3.X > rightCutoff))
                {
                    continue;
                }

                if (!hideHorizontalSegments_)
                {
                    if (scale_ != 1.0f)
                    {
                        float middle = (xPos2.X + xPos1.X)/2.0f;
                        float width = xPos2.X - xPos1.X;
                        width *= scale_;
                        g.DrawLine(Pen, (int) (middle - width/2.0f), yPos1.Y, (int) (middle + width/2.0f), yPos2.Y);
                    }
                    else
                    {
                        g.DrawLine(Pen, xPos1.X, yPos1.Y, xPos2.X, yPos2.Y);
                    }
                }

                if (!hideVerticalSegments_)
                {
                    g.DrawLine(Pen, xPos2.X, yPos2.Y, xPos3.X, yPos3.Y);
                }
            }
        }

        /// <summary>
        /// Returns an X-axis suitable for use by this plot. The axis will be one that is just long
        /// enough to show all data.
        /// </summary>
        /// <returns>X-axis suitable for use by this plot.</returns>
        public Axis SuggestXAxis()
        {
            SequenceAdapter data =
                new SequenceAdapter(DataSource, DataMember, OrdinateData, AbscissaData);

            if (data.Count < 2)
            {
                return data.SuggestXAxis();
            }

            // else

            Axis a = data.SuggestXAxis();

            PointD p1 = data[0];
            PointD p2 = data[1];
            PointD p3 = data[data.Count - 2];
            PointD p4 = data[data.Count - 1];

            double offset1;
            double offset2;

            if (!center_)
            {
                offset1 = 0.0f;
                offset2 = p4.X - p3.X;
            }
            else
            {
                offset1 = (p2.X - p1.X)/2.0f;
                offset2 = (p4.X - p3.X)/2.0f;
            }

            a.WorldMin -= offset1;
            a.WorldMax += offset2;

            return a;
        }

        /// <summary>
        /// Returns an Y-axis suitable for use by this plot. The axis will be one that is just long
        /// enough to show all data.
        /// </summary>
        /// <returns>Y-axis suitable for use by this plot.</returns>
        public Axis SuggestYAxis()
        {
            SequenceAdapter data =
                new SequenceAdapter(DataSource, DataMember, OrdinateData, AbscissaData);

            return data.SuggestYAxis();
        }

        /// <summary>
        /// Draws a representation of this plot in the legend.
        /// </summary>
        /// <param name="g">The graphics surface on which to draw.</param>
        /// <param name="startEnd">A rectangle specifying the bounds of the area in the legend set aside for drawing.</param>
        public virtual void DrawInLegend(Graphics g, Rectangle startEnd)
        {
            g.DrawLine(pen_, startEnd.Left, (startEnd.Top + startEnd.Bottom)/2,
                       startEnd.Right, (startEnd.Top + startEnd.Bottom)/2);
        }
    }
}